/**
 * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see
 * <a href="https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project">https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project</a>.
 *
 * Copyright (c) 2014 - The OWASP Foundation
 *
 * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software.
 *
 * @author Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a> - Original ESAPI author
 * @author August Detlefsen <a href="http://www.codemagi.com">CodeMagi</a> - Java File IO Security Project lead
 * @created 2014
 */
package org.owasp.fileio.util;

import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Random;

/**
 * Utilities to help with tests that involve files or directories.
 */
public class FileTestUtils {

    private static final Class CLASS = FileTestUtils.class;
    private static final String CLASS_NAME = CLASS.getName();
    private static final String DEFAULT_PREFIX = CLASS_NAME + '.';
    private static final String DEFAULT_SUFFIX = ".tmp";
    private static final Random rand;

    /*
     Rational for switching from SecureRandom to Random:
		
     This is used for generating filenames for temporary
     directories. Origionally this was using SecureRandom for
     this to make /tmp races harder. This is not necessary as
     mkdir always returns false if if the directory already
     exists.
		
     Additionally, SecureRandom for some reason on linux
     is appears to be reading from /dev/random instead of
     /dev/urandom. As such, the many calls for temporary
     directories in the unit tests quickly depleates the
     entropy pool causing unit test runs to block until more
     entropy is collected (this is why moving the mouse speeds
     up unit tests).
     */
    static {
	SecureRandom secRand = new SecureRandom();
	rand = new Random(secRand.nextLong());
    }

    /**
     * Private constructor as all methods are static.
     */
    private FileTestUtils() {
    }

    /**
     * Convert a long to it's hex representation. Unlike
	 * {
     *
     * @ Long#toHexString(long)} this always returns 16 digits.
     * @param l The long to convert.
     * @return l in hex.
     */
    public static String toHexString(long l) {
	String initial;
	StringBuffer sb;

	initial = Long.toHexString(l);
	if (initial.length() == 16) {
	    return initial;
	}
	sb = new StringBuffer(16);
	sb.append(initial);
	while (sb.length() < 16) {
	    sb.insert(0, '0');
	}
	return sb.toString();
    }

    /**
     * Create a temporary directory.
     *
     * @param parent The parent directory for the temporary directory. If this is null, the system property "java.io.tmpdir" is used.
     * @param prefix The prefix for the directory's name. If this is null, the full class name of this class is used.
     * @param suffix The suffix for the directory's name. If this is null, ".tmp" is used.
     * @return The newly created temporary directory.
     * @throws IOException if directory creation fails
     * @throws SecurityException if {@link File#mkdir()} throws one.
     */
    public static File createTmpDirectory(File parent, String prefix, String suffix) throws IOException {
	String name;
	File dir;

	if (prefix == null) {
	    prefix = DEFAULT_PREFIX;
	} else if (!prefix.endsWith(".")) {
	    prefix += '.';
	}
	if (suffix == null) {
	    suffix = DEFAULT_SUFFIX;
	} else if (!suffix.startsWith(".")) {
	    suffix = "." + suffix;
	}
	if (parent == null) {
	    parent = new File(System.getProperty("java.io.tmpdir"));
	}
	name = prefix + toHexString(rand.nextLong()) + suffix;
	dir = new File(parent, name);
	if (!dir.mkdir()) {
	    throw new IOException("Unable to create temporary directory " + dir);
	}
	return dir.getCanonicalFile();
    }

    /**
     * Create a temporary directory. This calls {@link #createTmpDirectory(File, String, String)} with null for parent and suffix.
     *
     * @param prefix The prefix for the directory's name. If this is null, the full class name of this class is used.
     * @return The newly created temporary directory.
     * @throws IOException if directory creation fails
     * @throws SecurityException if {@link File#mkdir()} throws one.
     */
    public static File createTmpDirectory(String prefix) throws IOException {
	return createTmpDirectory(null, prefix, null);
    }

    /**
     * Create a temporary directory. This calls {@link #createTmpDirectory(File, String, String)} with null for all arguments.
     *
     * @return The newly created temporary directory.
     * @throws IOException if directory creation fails
     * @throws SecurityException if {@link File#mkdir()} throws one.
     */
    public static File createTmpDirectory() throws IOException {
	return createTmpDirectory(null, null, null);
    }

    /**
     * Checks that child is a directory and really a child of parent. This verifies that the {@link File#getCanonicalFile()
     * canonical} child is actually a child of parent. This should fail if the child is a symbolic link to another directory and therefore should not be traversed in a recursive traversal of a
     * directory.
     *
     * @param parent The supposed parent of the child
     * @param child The child to check
     * @return true if child is a directory and a direct decendant of parent.
     * @throws IOException if {@link File#getCanonicalFile()} does
     * @throws NullPointerException if either parent or child are null.
     */
    public static boolean isChildSubDirectory(File parent, File child) throws IOException {
	File childsParent;

	if (child == null) {
	    throw new NullPointerException("child argument is null");
	}
	if (!child.isDirectory()) {
	    return false;
	}
	if (parent == null) {
	    throw new NullPointerException("parent argument is null");
	}
	parent = parent.getCanonicalFile();
	child = child.getCanonicalFile();
	childsParent = child.getParentFile();
	if (childsParent == null) {
	    return false;	// sym link to /?
	}
	childsParent = childsParent.getCanonicalFile();	// just in case...
	if (!parent.equals(childsParent)) {
	    return false;
	}
	return true;
    }

    /**
     * Delete a file. Unlinke {@link File#delete()}, this throws an exception if deletion fails.
     *
     * @param file The file to delete
     * @throws IOException if file is not null, exists but delete fails.
     */
    public static void delete(File file) throws IOException {
	if (file == null || !file.exists()) {
	    return;
	}
	if (!file.delete()) {
	    throw new IOException("Unable to delete file " + file.getAbsolutePath());
	}
    }

    /**
     * Recursively delete a file. If file is a directory, subdirectories and files are also deleted. Care is taken to not traverse symbolic links in this process. A null file or a file that does not
     * exist is considered to already been deleted.
     *
     * @param file The file or directory to be deleted
     * @throws IOException if the file, or a descendant, cannot be deleted.
     * @throws SecurityException if {@link File#delete()} does.
     */
    public static void deleteRecursively(File file) throws IOException {
	File[] children;
	File child;

	if (file == null || !file.exists()) {
	    return;	// already deleted?
	}
	if (file.isDirectory()) {
	    children = file.listFiles();
	    for (int i = 0; i < children.length; i++) {
		child = children[i];
		if (isChildSubDirectory(file, child)) {
		    deleteRecursively(child);
		} else {
		    delete(child);
		}
	    }
	}

	// finally
	delete(file);
    }
}
